#include "openglpainter.h"
#include "gpu_engine/utils/TimeUtils.h"
#include "gpu_engine/log/Log.h"
#include "gpu_engine/GPUBezierThickLineGenerator.h"


OpenGLPainter::OpenGLPainter() {
    context = new QOpenGLContext();
    if (!context->create()) {
        qFatal("failed to create opengl context!");
    }

    surface = new QOffscreenSurface();
    surface->setFormat(context->format());
    surface->create();

    if (!context->makeCurrent(surface)) {
        qFatal("failed to active opengl context!");
    }
    f = context->functions();
    ef = context->extraFunctions();

    device = new QOpenGLPaintDevice();

    device->setSize(drawSize);

    QOpenGLFramebufferObjectFormat fboFormat;
    fboFormat.setSamples(4);
    fboFormat.setAttachment(QOpenGLFramebufferObject::CombinedDepthStencil);
    fbo = new QOpenGLFramebufferObject(drawSize, fboFormat);

    QOpenGLFramebufferObjectFormat resolveFboFormat;
    resolveFboFormat.setSamples(0);
    resolveFboFormat.setAttachment(QOpenGLFramebufferObject::CombinedDepthStencil);
    resolveFbo = new QOpenGLFramebufferObject(drawSize, resolveFboFormat);

    f->glGenBuffers(1, &pbo);
    f->glBindBuffer(GL_PIXEL_PACK_BUFFER, pbo);
    f->glBufferData(GL_PIXEL_PACK_BUFFER, drawSize.width() * drawSize.height() * 4, NULL, GL_STREAM_READ);
    f->glBindBuffer(GL_PIXEL_PACK_BUFFER, 0);
}

OpenGLPainter::~OpenGLPainter() {
    if (context && surface && context->makeCurrent(surface)) {
        f->glBindBuffer(GL_PIXEL_PACK_BUFFER, pbo);
        if (data) {
            ef->glUnmapBuffer(GL_PIXEL_PACK_BUFFER);
            data = nullptr;
        }
        f->glDeleteBuffers(1, &pbo);
        f->glBindBuffer(GL_PIXEL_PACK_BUFFER, 0);
        delete fbo;
        delete resolveFbo;
        delete device;
        context->doneCurrent();
    }
    delete surface;
    delete context;
}

QImage OpenGLPainter::Paint(size_t line, size_t nums) {
    f->glBindFramebuffer(GL_FRAMEBUFFER, fbo->handle());
    f->glViewport(0, 0, drawSize.width(), drawSize.height());
    f->glClearColor(0.0f, 0.0f, 0.0f, 0.0f);
    f->glClear(GL_COLOR_BUFFER_BIT);

    QPainter painter;
    bool success = painter.begin(device);
    if (!success) {
        qFatal() << "QPainter::begin() failed!";
    } else {
        QBrush brush;
        brush.setStyle(Qt::BrushStyle::SolidPattern);

        BezierParams bezierParams{};
        bezierParams.lineNums = line;
        bezierParams.bodyPointsNums = nums;
        bezierParams.assPointsNums = 30;
        bezierParams.headPointsNums = 10;

        static GPUBezierThickLineGenerator utils;

        if (!initialized) {
            if (!utils.InitializeGPUPipeline(bezierParams)) {
                Logger() << "Failed to initialize GPU pipeline." << std::endl;
                exit(1);
            }
            initialized = true;
        }

        std::vector<BezierLine> lines{};
        for (uint32_t i = 0; i < bezierParams.lineNums; i++) {
            BezierLine line{
                            .points = {
                                       {.x = 100.0, .y = 100 + (1024.0f / bezierParams.lineNums) * static_cast<float>(i)},
                                       {.x = 300.0, .y = 20.0},
                                       {.x = 800.0, .y = 100.0},
                                       {.x = 800.0, .y = 700.0},
                                       },
                            .beginWidth = 100.0f,
                            .endWidth = 1.0f,
                            };
            lines.push_back(line);
        }

        const uint64_t now = TimeUtils::GetCurrentMonoMs();
        const Point2D *points = utils.GenerateThickLine(lines);
        const uint64_t last = TimeUtils::GetCurrentMonoMs();

        std::cout << "Totally Usage: " << last - now << " ms" << std::endl;
        const size_t pointsNums = bezierParams.bodyPointsNums + bezierParams.assPointsNums + bezierParams.headPointsNums;

        for (uint32_t lineIdx = 0; lineIdx < bezierParams.lineNums; lineIdx++) {
            brush.setColor(QColor(rand() % 255, rand() % 255, rand() % 255));
            QPainterPath path;
            for (uint32_t pointIdx = 0; pointIdx < pointsNums * 2; pointIdx++) {
                const uint32_t offset = lineIdx * pointsNums  * 2;
                const auto [x, y] = points[offset + pointIdx];
                if (pointIdx == 0) {
                    path.moveTo({x,y});
                } else {
                    path.lineTo({x,y});
                }
            }

            path.closeSubpath();
            painter.fillPath(path, brush);
        }

        painter.end();
    }

    f->glBindFramebuffer(GL_READ_FRAMEBUFFER, fbo->handle());
    f->glBindFramebuffer(GL_DRAW_FRAMEBUFFER, resolveFbo->handle());
    ef->glBlitFramebuffer(0, 0, drawSize.width(), drawSize.height(),
                          0, 0, drawSize.width(), drawSize.height(),
                          GL_COLOR_BUFFER_BIT, GL_NEAREST);

    f->glBindFramebuffer(GL_READ_FRAMEBUFFER, resolveFbo->handle());
    f->glBindBuffer(GL_PIXEL_PACK_BUFFER, pbo);
    f->glReadPixels(0, 0, drawSize.width(), drawSize.height(), GL_RGBA, GL_UNSIGNED_BYTE, 0);
    f->glBindBuffer(GL_PIXEL_PACK_BUFFER, 0);

    f->glBindBuffer(GL_PIXEL_PACK_BUFFER, pbo);
    GLint currentBuffer;
    f->glGetIntegerv(GL_PIXEL_PACK_BUFFER_BINDING, &currentBuffer);
    if (currentBuffer != pbo) {
        qDebug() << "PBO not bound before mapping!";
        f->glBindBuffer(GL_PIXEL_PACK_BUFFER, pbo);
    }
    data = static_cast<uint32_t *>(ef->glMapBufferRange(GL_PIXEL_PACK_BUFFER, 0, drawSize.width() * drawSize.height() * 4, GL_MAP_READ_BIT));
    GLenum err = f->glGetError();
    while(err != GL_NO_ERROR) {
        if (err == GL_INVALID_OPERATION) {
            std::cout << "Invalid Operation" << std::endl;
        } else {
            std::cout << "Error:" << err << std::endl;
        }
        err = f->glGetError();
    }
    f->glBindBuffer(GL_PIXEL_PACK_BUFFER, 0);

    return QImage((uchar *) data, drawSize.width(), drawSize.height(), QImage::Format_RGBA8888);
}
